/**
 * @class 资源管理器
 * @author DuskyHuang 圣子
 * @description  提供资源的全局持久化和存取功能（可在Root面板上设置）。
*/

import { assetManager, ImageAsset, JsonAsset, Prefab, resources, Sprite, SpriteFrame, sys, Texture2D } from 'cc';
import { MINIGAME } from 'cc/env';
import { singleton } from '../utils/decorators';

@singleton
export default class ResManager {

    declare public static readonly instance: ResManager;

    /** 
     * 获取图集
     * @param name 图集名称
     * @return 图集
     */
    public getSheet(name: string): SpriteFrame[] { return this._sheets.get(name); }

    /** 
     * 判断是否含有图集
     * @param name 图集名称
     * @return 判断结果
     */
    public hasSheet(name: string): bool.res { return this._sheets.has(name); }

    /** 
     * 增加图集
     * @param name 图集名称
     * @param sheet 图集内容
     * @param override 是否覆盖现有同名内容，默认覆盖
     * @return 自身
     */
    public addSheet(name: string, sheet: SpriteFrame[], override: boolean = true): this {
        if (this.hasSheet(name) && !override) return this;
        this._sheets.set(name, sheet);
        return this;
    }

    /** 
     * 移除图集
     * @param name 图集名称
     * @return 是否存在移除内容
     */
    public removeSheet(name: string): bool.exist {
        if (this._sheets.has(name)) {
            this._sheets.delete(name);
            return true;
        } else return false;
    }

    /** 清空所有图集 */
    public clearSheets(): this { this._sheets.clear(); return this; }

    /** 
     * 为图集增加图片
     * @param name 图集名称
     * @param frames 图片罗列
     * @return 自身
     */
    public addSheetFrames(name: string, ...frames: SpriteFrame[]): this {
        const exist = this._sheets.has(name);
        if (!exist) this._sheets.set(name, []);
        const sheet = this._sheets.get(name)!;
        sheet.pushUnique(...frames);
        return this;
    }

    /** 
     * 从图集获取图片
     * @param name 图集名称
     * @param key 图片名称
     * @return 图片
     */
    public getSheetFrame(name: string, key: string): SpriteFrame | null {
        const sheet = this._sheets.get(name);
        if (sheet) {
            for (let frame of sheet) { if (frame.name === key) return frame; }
        } return null;
    }

    /** 
     * 获取配置
     * @param name 配置名称
     * @return 配置对象
     */
    public getConfig<T extends object>(name: string): T | null { return this._configs.get(name) as T; }

    /** 
     * 判断是否含有配置
     * @param name 配置名称
     * @return 判断结果
     */
    public hasConfig(name: string): bool.res { return this._configs.has(name); }

    /** 
     * 增加配置
     * @param name 配置名称
     * @param config 配置对象
     * @param override 是否覆盖现有同名内容，默认覆盖
     * @return 自身
     */
    public addConfig(name: string, config: object, override: boolean = true): this {
        if (this._configs.has(name) && !override) return this;
        this._configs.set(name, config);
        return this;
    }

    /** 
     * 移除配置
     * @param name 配置名称
     * @return 是否存在移除内容
     */
    public removeConfig(name: string): bool.res {
        if (this._configs.has(name)) {
            this._configs.delete(name);
            return true;
        } else return false;
    }

    /** 清空所有配置 */
    public clearConfigs(): this { this._configs.clear(); return this; }

    /** 
     * 获取预制体
     * @param name 预制体名称
     * @return 预制体对象
     */
    public getPrefab(name: string): Prefab | null { return this._prefabs.get(name); }

    /** 
     * 判断是否含有预制体
     * @param name 预制体名称
     * @return 判断结果
     */
    public hasPrefab(name: string): bool.res { return this._prefabs.has(name); }

    /** 
     * 增加预制体
     * @param name 预制体名称
     * @param prefab 预制体对象
     * @param override 是否覆盖现有同名内容，默认覆盖
     * @return 自身
     */
    public addPrefab(name: string, prefab: Prefab, override: boolean = true): this {
        if (this._prefabs.has(name) && !override) return this;
        this._prefabs.set(name, prefab);
        return this;
    }

    /** 
     * 移除预制体
     * @param name 预制体名称
     * @param destroy 是否销毁，默认是
     * @return 是否存在移除内容
     */
    public removePrefab(name: string, destroy: boolean = true): bool.res {
        if (this._prefabs.has(name)) {
            const prefab = this._prefabs.get(name);
            this._prefabs.delete(name);
            if (destroy) prefab.destroy();
            return true;
        } else return false;
    }

    /** 清空所有预制体 */
    public clearPrefabs(): this {
        this._prefabs.forEach(prefab => prefab.destroy());
        this._prefabs.clear(); return this;
    }

    /** 
     * 为配置增加条目
     * @param name 配置名称
     * @param key 条目名称
     * @param value 条目内容
     * @param override 是否覆盖已有同名内容，默认覆盖
     * @return 自身
     */
    public addConfigItem(name: string, key: string, value: any, override: boolean = true): this {
        const config = this._configs.get(name);
        if (config) {
            const exist = !!config[key];
            if (exist && !override) return this;
            config[key] = value;
        }
        return this;
    }

    /** 
     * 从配置获取条目
     * @param name 配置名称
     * @param key 条目名称
     * @return 配置条目
     */
    public getConfigItem<T extends any>(name: string, key: string): T {
        const config = this._configs.get(name);
        return config ? config[key] as T : null;
    }

    /** 
     * 加载动态资源目录下图集资源
     * @param dir resource路径下目录
     * @param name 为图集设置名称
     * @param override 是否覆盖已有同名资源，默认覆盖
     * @return 图集资源内容
     */
    public async loadResSheetByDir(dir: string, name: string, override: boolean = true): Promise<SpriteFrame[] | null> {
        if (this.hasSheet(name) && !override) return null;
        return new Promise(resolve => {
            resources.loadDir(dir, SpriteFrame, (err, frames) => {
                if (err) { console.warn(err); return resolve(null); }
                else {
                    this._sheets.set(name, frames);
                    return resolve(frames);
                }
            });
        });
    }

    /** 
     * 加载动态资源目录下所有配置资源
     * @param dir resource路径下目录
     * @param override 是否覆盖已有同名资源，默认覆盖
     * @return 所有配置资源
     */
    public async loadResConfigsByDir(dir: string, override: boolean = true): Promise<JsonAsset[] | null> {
        if (this._config_dirs.contains(dir) && !override) return null;
        this._config_dirs.pushUnique(dir);
        return new Promise(resolve => {
            resources.loadDir(dir, JsonAsset, (err, jsons) => {
                if (err) { console.warn(err); return resolve(null); }
                else {
                    jsons.forEach(json => this._configs.set(json.name, json.json));
                    return resolve(jsons);
                }
            });
        });
    }

    /** 
     * 加载动态资源目录下指定图片帧资源
     * @param path resource路径下路径
     * @param sprite 设置为指定图片的帧
     * @return 图片帧
     */
    public async loadResImage(path: string, sprite?: Sprite): Promise<SpriteFrame | null> {
        return new Promise(resolve => {
            resources.load(`${path}/spriteFrame`, SpriteFrame, (err, frame) => {
                if (err) { console.warn(err); return resolve(null); }
                else { if (sprite) sprite.spriteFrame = frame; return resolve(frame); }
            });
        });
    }

    /** 
     * 设置远程BaseUrl
     * @param base BaseUrl eg.`https://xxx-cloud1-xxxx-xxxxx.tcb.qcloud.la/xxx`
     */
    public setRemoteBaseUrl(base: str.url): this { this._remoteBaseUrl = base; return this; }

    /** 
     * 设置远程Bundle名称
     * @param name 远程包名称
     */
    public setRemoteBundleName(name: string): this { this._remoteBundleName = name; return this; }

    /** 
     * 获取远程纹理
     * @param url 资源路径
     * @param type 资源类型，默认`jpg`
     * @return 远程纹理
     * @explain 小游戏环境中优先尝试从BaseUrl下获取纹理，否则从远程远程包下获取纹理，否则从动态图集目录下获取纹理
     */
    public async loadRemoteTexture(url: string, type: str.ext = 'jpg'): Promise<Texture2D> {
        const { _remoteBundleName: name, _remoteBaseUrl: base } = this;
        return new Promise((resolve, reject) => {
            if (MINIGAME && !!base) {
                const fullUrl = `${this._remoteBaseUrl}/${url}.${type}`;
                assetManager.loadRemote<ImageAsset>(fullUrl, (err, imageAsset) => {
                    if (err) {
                        console.warn(`[loadRemoteTexture]: remote.url="${url}" failed`, err);
                        reject(null);
                    } else {
                        const tex = new Texture2D();
                        tex.image = imageAsset;
                        resolve(tex);
                    }
                });
            } else if (!!name) {
                const bundle = assetManager.getBundle(name);
                if (!bundle) {
                    console.warn(`[loadRemoteTexture]: bundle "${url}" not found`);
                    reject(null);
                } else bundle.load(`${url}/texture`, Texture2D, (err, tex) => {
                    if (err) {
                        console.warn(`[loadRemoteTexture]: bundle.url="${url}" failed`, err);
                        reject(null);
                    } else resolve(tex);
                });
            } else {
                resources.load(`${url}/texture`, Texture2D, (err, tex) => {
                    if (err) {
                        console.warn(`[loadRemoteTexture]: res.url="${url}" failed`, err);
                        reject(null);
                    } else resolve(tex);
                });
            }
        });
    }

    private _configs: Map<string, object> = new Map;
    private _sheets: Map<string, SpriteFrame[]> = new Map;
    private _config_dirs: string[] = [];
    private _remoteBundleName: string;
    private _remoteBaseUrl: string;
    private _prefabs: Map<string, Prefab> = new Map;
}